Skip to content

[client] Add PCP support#5219

Merged
lixmal merged 2 commits intomainfrom
pcp-support
Apr 15, 2026
Merged

[client] Add PCP support#5219
lixmal merged 2 commits intomainfrom
pcp-support

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Jan 30, 2026

Describe your changes

This adds PCP (Port Control Protocol) support. PCP is discovered first and falls back to NAT-PMP/UPnP if unavailable.

PCP's advantages over NAT-PMP:

  • Can open ports through CGNAT (carrier-grade NAT) if supported by the ISP
  • Supports IPv6 pinholes for dual-stack environments

Key features:

  • Dual-stack: Automatically creates both IPv4 mappings and IPv6 pinholes when available
  • Epoch monitoring: Detects router restarts via the PCP epoch field and automatically recreates mappings
  • Health checks: Periodic ANNOUNCE requests verify server responsiveness (every minute)

New environment variables:

  • NB_DISABLE_NAT_MAPPER - disable NAT port mapping entirely
  • NB_DISABLE_PCP_HEALTH_CHECK - disable PCP health monitoring

New logs:

2026-01-30T09:04:43Z DEBG client/internal/portforward/pcp/nat.go:157: PCP IPv6 gateway discovered: 2001:db8:a::1 (local: 2001:db8:a::10)
2026-01-30T09:04:43Z INFO client/internal/portforward/manager.go:136: discovered NAT gateway: PCP
2026-01-30T09:04:43Z DEBG client/internal/portforward/pcp/nat.go:81: IPv6 PCP pinhole created for port 51820
2026-01-30T09:04:43Z INFO client/internal/portforward/manager.go:212: created port mapping: 51820 -> 51820 via PCP (external IP: 45.45.45.1)
2026-01-30T11:03:43Z TRAC client/internal/portforward/manager.go:333: PCP health check passed (epoch=7172)
2026-01-30T11:04:43Z DEBG client/internal/portforward/pcp/nat.go:81: IPv6 PCP pinhole created for port 51820
2026-01-30T11:04:43Z DEBG client/internal/portforward/manager.go:367: renewed port mapping: 51820 -> 51820

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • PCP (Port Control Protocol) support with IPv4/IPv6 dual-stack mappings and PCP-first gateway discovery (falls back to NAT-PMP/UPnP).
  • Improvements

    • Periodic PCP health checks (toggle via NB_DISABLE_PCP_HEALTH_CHECK) detect gateway restarts, trigger immediate mapping renewal, and handle permanent mappings more robustly.
  • Tests

    • Comprehensive unit tests plus a skip-by-default integration test covering PCP behaviors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d4efa39-7320-4137-89f4-b664a94c6d94

📥 Commits

Reviewing files that changed from the base of the PR and between 3b287ee and d5b5056.

📒 Files selected for processing (1)
  • client/internal/portforward/manager.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/internal/portforward/manager.go

📝 Walkthrough

Walkthrough

Adds PCP (Port Control Protocol) support: protocol primitives, a concurrent PCP client, a PCP-backed NAT adapter with discovery, PCP-first gateway discovery with fallback, and manager integration for periodic PCP health checks that can trigger mapping recreation; unit tests for the PCP client included.

Changes

Cohort / File(s) Summary
PCP protocol & helpers
client/internal/portforward/pcp/protocol.go
Adds RFC 6887 constants, opcodes, result codes, request/response formats, and IPv4/IPv6 address helpers and parsers.
PCP client implementation & tests
client/internal/portforward/pcp/client.go, client/internal/portforward/pcp/client_test.go
Introduces a concurrent-safe PCP Client with Announce/Map/Delete/GetExternalAddress, epoch/state-loss handling, retry/backoff and deadline logic, cached external IP behavior, and unit/integration tests.
PCP NAT adapter & discovery
client/internal/portforward/pcp/nat.go
Implements nat.NAT over PCP (IPv4 primary, optional IPv6), Add/Delete mapping flows, CheckServerHealth (ANNOUNCE/epoch), and DiscoverPCP using system routing.
Portforward manager integration
client/internal/portforward/manager.go
Adds PCP health-check logic: separate health and renewal tickers, checkHealthAndRecreate to detect server restarts and trigger immediate mapping recreation and renewal-ticker reset; permanent mapping loop now runs health checks.
Discovery wiring & state/env tweaks
client/internal/portforward/state.go, client/internal/portforward/env.go
Replaces direct NAT discovery with PCP-first defaultDiscoverGateway fallback to existing discovery; adds NB_DISABLE_PCP_HEALTH_CHECK env flag and generalized boolean env parsing.
Small edits
client/internal/portforward/manager.go (other lines)
Replaced nat.DiscoverGateway usage with discoverGateway; removed an inline TODO in createMapping when GetExternalAddress fails.

Sequence Diagram(s)

sequenceDiagram
    participant Mgr as PortForward Manager
    participant HT as Health Ticker
    participant RT as Renew Ticker
    participant PCP as PCP Client
    participant GW as PCP Gateway

    HT->>Mgr: health tick
    Mgr->>PCP: Announce(ctx)
    PCP->>GW: ANNOUNCE request
    GW-->>PCP: ANNOUNCE response (epoch)
    PCP-->>Mgr: epoch, stateLost?

    alt server restarted (stateLost)
        Mgr->>PCP: AddPortMapping(ctx) (recreate)
        PCP->>GW: MAP request
        GW-->>PCP: MAP response (external port/IP)
        PCP-->>Mgr: mapping info
        Mgr->>RT: reset renew ticker
    else no restart
        Mgr->>Mgr: continue
    end

    RT->>Mgr: renew tick
    Mgr->>PCP: AddPortMapping(ctx) (renew)
    PCP->>GW: MAP request
    GW-->>PCP: MAP response
    PCP-->>Mgr: updated mapping
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • crn4

Poem

🐰 I nibble bytes where gateways sleep,

I count epochs in the tunnels deep.
I map the ports, then watch the beat,
If servers stir, I fix the street.
A hop, a ping — PCP makes nets complete.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers what changed, key features, environment variables, and example logs. However, it lacks issue ticket reference and does not indicate whether tests were created for the changes. Consider adding an issue ticket reference and clarifying whether tests were created to validate the PCP support implementation, especially given the added unit tests in the raw summary.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title '[client] Add PCP support' is clear and directly describes the main feature addition of this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pcp-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented Jan 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@client/internal/portforward/pcp/client_test.go`:
- Around line 161-187: The test TestClientIntegration unconditionally calls
t.Skip; change it to read PCP_TEST_GATEWAY and PCP_TEST_LOCAL_IP from the
environment and only skip when those vars are not set (or are invalid). Parse
PCP_TEST_GATEWAY and PCP_TEST_LOCAL_IP into netip.Addr (or use
netip.MustParseAddr if you prefer panicking on invalid values), create the
client with NewClient(parsedGateway), call client.SetLocalIP(parsedLocalIP), and
then run the ANNOUNCE / AddPortMapping / DeletePortMapping logic as before;
ensure you use context.WithTimeout and return early with t.Skipf when env vars
are absent or parsing fails to allow manual runs without code edits.

In `@client/internal/portforward/pcp/client.go`:
- Around line 136-199: addPortMappingWithHint currently casts ports to uint16
without validating inputs; add explicit range checks before calling
buildMapRequest: ensure internalPort is within 1..65535 (reject and return an
error if out of range) and validate suggestedExtPort is within 0..65535 (or
1..65535 if you decide 0 should be invalid in your protocol), then only proceed
to call buildMapRequest and sendRequest; place these checks at the start of
addPortMappingWithHint (before the uint16 casts) and return clear fmt.Errorf
messages referencing internalPort/suggestedExtPort when invalid.
🧹 Nitpick comments (1)
client/internal/portforward/pcp/nat.go (1)

19-34: Track the IPv6 address-rotation TODO.

Worth capturing as a follow-up issue or task so pinholes are refreshed if the local IPv6 changes.

Comment thread client/internal/portforward/pcp/client_test.go
Comment thread client/internal/portforward/pcp/client.go
@lixmal
Copy link
Copy Markdown
Collaborator Author

lixmal commented Jan 30, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 30, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lixmal lixmal force-pushed the nat-pmp-upnp branch 2 times, most recently from 9b45979 to 7fa7f1c Compare January 30, 2026 14:52
@sonarqubecloud
Copy link
Copy Markdown

1 similar comment
@sonarqubecloud
Copy link
Copy Markdown

Base automatically changed from nat-pmp-upnp to main April 8, 2026 07:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
client/internal/portforward/pcp/client.go (1)

45-67: Consider returning an error for invalid gateway IPs.

The constructors log invalid IPs at debug level but continue with a zero-value netip.Addr. While callers like DiscoverPCP validate the gateway beforehand, direct callers of NewClient with an invalid IP will silently fail later during sendRequest. Consider returning an error or panicking to fail fast.

♻️ Suggested defensive approach
 func NewClient(gateway net.IP) *Client {
 	addr, ok := netip.AddrFromSlice(gateway)
 	if !ok {
-		log.Debugf("invalid gateway IP: %v", gateway)
+		log.Warnf("invalid gateway IP: %v", gateway)
+		return nil
 	}
 	return &Client{
 		gateway: addr.Unmap(),
 		timeout: defaultTimeout,
 	}
 }

Alternatively, keep the current behavior if you prefer graceful degradation, but ensure callers handle the nil case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/internal/portforward/pcp/client.go` around lines 45 - 67, NewClient
and NewClientWithTimeout currently accept an invalid gateway IP silently
(logging only) and return a Client with a zero-value netip.Addr which will fail
later (e.g., in sendRequest); change the constructors to return (*Client, error)
instead of *Client (update NewClient and NewClientWithTimeout signatures),
validate netip.AddrFromSlice(gateway) and return a descriptive error when
ok==false, and adjust callers (e.g., places that call DiscoverPCP or other
direct callers) to handle the error; alternatively, if you prefer panic
semantics, document and call panic on invalid gateway inside
NewClient/NewClientWithTimeout so clients fail fast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@client/internal/portforward/pcp/client.go`:
- Around line 45-67: NewClient and NewClientWithTimeout currently accept an
invalid gateway IP silently (logging only) and return a Client with a zero-value
netip.Addr which will fail later (e.g., in sendRequest); change the constructors
to return (*Client, error) instead of *Client (update NewClient and
NewClientWithTimeout signatures), validate netip.AddrFromSlice(gateway) and
return a descriptive error when ok==false, and adjust callers (e.g., places that
call DiscoverPCP or other direct callers) to handle the error; alternatively, if
you prefer panic semantics, document and call panic on invalid gateway inside
NewClient/NewClientWithTimeout so clients fail fast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9139a537-e03a-449f-be91-853e11752ba1

📥 Commits

Reviewing files that changed from the base of the PR and between 9983c60 and 3b287ee.

📒 Files selected for processing (7)
  • client/internal/portforward/env.go
  • client/internal/portforward/manager.go
  • client/internal/portforward/pcp/client.go
  • client/internal/portforward/pcp/client_test.go
  • client/internal/portforward/pcp/nat.go
  • client/internal/portforward/pcp/protocol.go
  • client/internal/portforward/state.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/internal/portforward/state.go
  • client/internal/portforward/pcp/client_test.go

@sonarqubecloud
Copy link
Copy Markdown

@lixmal lixmal merged commit 0d86de4 into main Apr 15, 2026
42 checks passed
@lixmal lixmal deleted the pcp-support branch April 15, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants